  function scanPerformanceForVideo() {
    try {
      const entries = performance.getEntriesByType('resource') || [];
      const urls = entries.map((e) => e.name).filter(Boolean);
      const videoUrls = urls.filter((u) => /\.(mp4|mov|webm)(\b|\?|#)/i.test(u) || /\.m3u8(\b|\?|#)/i.test(u));
      // prefer xhscdn mp4
      const sorted = videoUrls.sort((a, b) => {
        const ax = /xhscdn\.com/i.test(a) ? 0 : 1;
        const bx = /xhscdn\.com/i.test(b) ? 0 : 1;
        if (ax !== bx) return ax - bx;
        const am = /\.mp4(\b|\?|#)/i.test(a) ? 0 : 1;
        const bm = /\.mp4(\b|\?|#)/i.test(b) ? 0 : 1;
        return am - bm;
      });
      return sorted;
    } catch { return []; }
  }

  function tapNetworkForVideoOnce(durationMs = 1500) {
    return new Promise((resolve) => {
      const found = new Set();
      const finish = () => resolve(Array.from(found));
      const timer = setTimeout(finish, Math.max(800, durationMs));
      const send = () => {
        try { clearTimeout(timer); finish(); } catch {}
      };
      const script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = chrome.runtime.getURL('net_tap.js');
      const handler = (e) => {
        if (!e || e.type !== 'xhsd-net-urls') return;
        window.removeEventListener('xhsd-net-urls', handler);
        try {
          const arr = JSON.parse(e.detail||'[]');
          arr.forEach((u)=>found.add(u));
        } catch {}
        send();
      };
      window.addEventListener('xhsd-net-urls', handler, { once: true });
      script.onload = () => {
        try {
          const ev = new CustomEvent('xhsd-start-net-tap', { detail: String(Math.max(800, durationMs)) });
          window.dispatchEvent(ev);
        } catch {}
        try { script.remove(); } catch {}
      };
      document.documentElement.appendChild(script);
    });
  }
  function scrapeUrlsFromPageContext() {
    return new Promise((resolve) => {
      const handler = (e) => {
        if (!e || e.type !== 'xhsd-video-urls') return;
        window.removeEventListener('xhsd-video-urls', handler);
        try {
          const data = JSON.parse(e.detail || '{}');
          resolve(data);
        } catch {
          resolve(null);
        }
      };
      window.addEventListener('xhsd-video-urls', handler, { once: true });

      // inject web-accessible script so it runs in page context (CSP-safe)
      const script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = chrome.runtime.getURL('page_scraper.js');
      script.onload = () => {
        try { script.remove(); } catch {}
      };
      document.documentElement.appendChild(script);
      setTimeout(() => resolve(null), 3000); // timeout
    });
  }
// content.js - inject Alt+click handler and progress UI

(function () {
  // Sidebar overlay to visualize batch queue in-page
  let overlayRoot = null;
  let overlayList = null;
  let overlayStatus = null;
  let overlayStartBtn = null;
  let overlayPauseBtn = null;
  let overlayClearBtn = null;

  function ensureOverlay() {
    if (overlayRoot && document.body.contains(overlayRoot)) return;
    overlayRoot = document.createElement('div');
    overlayRoot.id = 'xhs-downloader-sidebar';
    overlayRoot.style.cssText = [
      'position:fixed',
      'top:0',
      'right:0',
      'width:320px',
      'height:100vh',
      'background:rgba(255,255,255,0.98)',
      'box-shadow:0 0 12px rgba(0,0,0,0.15)',
      'border-left:1px solid #e5e7eb',
      'z-index:2147483646',
      'display:flex',
      'flex-direction:column',
      'font-family: -apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,Helvetica,Arial,sans-serif',
      'font-size:12px',
      'color:#111827'
    ].join(';');

    const header = document.createElement('div');
    header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid #e5e7eb;background:#fff;';
    const title = document.createElement('div');
    title.textContent = '下载队列';
    title.style.cssText = 'font-weight:600;font-size:13px;';
    overlayStatus = document.createElement('div');
    overlayStatus.style.cssText = 'font-size:11px;color:#4b5563;margin-left:12px;flex:1;text-align:left;'; // Adjusted styles
    const closeBtn = document.createElement('button');
    closeBtn.textContent = '隐藏';
    closeBtn.style.cssText = 'border:1px solid #d1d5db;background:#f9fafb;border-radius:4px;padding:2px 6px;cursor:pointer;font-size:11px;';
    closeBtn.addEventListener('click', () => { overlayRoot.style.display = 'none'; });
    // Controls: Start, Pause, Clear
    const controls = document.createElement('div');
    controls.style.cssText = 'display:flex;gap:6px;align-items:center;'; // Removed margin-left
    overlayStartBtn = document.createElement('button');
    overlayStartBtn.textContent = '开始下载';
    overlayStartBtn.style.cssText = 'border:1px solid #10b981;color:#065f46;background:#d1fae5;border-radius:4px;padding:2px 6px;cursor:pointer;font-size:11px;';
    overlayPauseBtn = document.createElement('button');
    overlayPauseBtn.textContent = '暂停';
    overlayPauseBtn.style.cssText = 'border:1px solid #f59e0b;color:#92400e;background:#fef3c7;border-radius:4px;padding:2px 6px;cursor:pointer;font-size:11px;';
    overlayClearBtn = document.createElement('button');
    overlayClearBtn.textContent = '清除完成';
    overlayClearBtn.style.cssText = 'border:1px solid #d1d5db;background:#f9fafb;border-radius:4px;padding:2px 6px;cursor:pointer;font-size:11px;';
    controls.appendChild(overlayStartBtn);
    controls.appendChild(overlayPauseBtn);
    controls.appendChild(overlayClearBtn);

    overlayStartBtn.addEventListener('click', () => {
      try { chrome.runtime.sendMessage({ type: 'START_QUEUE' }, () => void chrome.runtime.lastError); } catch {}
    });
    overlayPauseBtn.addEventListener('click', () => {
      try { chrome.runtime.sendMessage({ type: 'PAUSE_QUEUE' }, () => void chrome.runtime.lastError); } catch {}
    });
    overlayClearBtn.addEventListener('click', () => {
      try {
        chrome.runtime.sendMessage({ type: 'CLEAR_COMPLETED_DOWNLOADS' }, () => {
          void chrome.runtime.lastError;
        });
      } catch {}
    });

    header.appendChild(title);
    header.appendChild(overlayStatus);
    header.appendChild(controls);
    header.appendChild(closeBtn);

    overlayList = document.createElement('div');
    overlayList.style.cssText = 'overflow:auto;flex:1;padding:8px;';

    const hint = document.createElement('div');
    hint.textContent = '提示：Ctrl+点击加入待下载；Alt+点击立即下载';
    hint.style.cssText = 'padding:6px 8px;color:#6b7280;border-bottom:1px dashed #e5e7eb;background:#f9fafb;';

    overlayRoot.appendChild(header);
    overlayRoot.appendChild(hint);
    overlayRoot.appendChild(overlayList);

    document.body.appendChild(overlayRoot);

    // Delegate remove clicks
    overlayList.addEventListener('click', (e) => {
      const btn = e.target.closest && e.target.closest('button');
      if (!btn || !btn.dataset.url) return;
      const url = btn.dataset.url;
      try {
        chrome.runtime.sendMessage({ type: 'REMOVE_FROM_QUEUE', url: url }, () => {
          void chrome.runtime.lastError;
        });
      } catch {}
    });

    // Small floating toggle button when hidden
    if (!document.getElementById('xhs-downloader-sidebar-toggle')) {
      const toggle = document.createElement('button');
      toggle.id = 'xhs-downloader-sidebar-toggle';
      toggle.textContent = '队列';
      toggle.style.cssText = 'position:fixed;right:8px;bottom:12px;z-index:2147483647;border:1px solid #d1d5db;background:#FFFFFF;border-radius:16px;padding:6px 10px;cursor:pointer;font-size:12px;box-shadow:0 2px 6px rgba(0,0,0,0.12)';
      toggle.addEventListener('click', () => {
        overlayRoot.style.display = 'flex';
      });
      document.body.appendChild(toggle);
    }
  }

  function renderSidebar(state) {
    // Do not auto-create overlay here; only render if already shown by SHOW_SIDEBAR
    if (!overlayRoot || !overlayList) return;
    const items = state && Array.isArray(state.queue) ? state.queue : [];
    const paused = !!(state && state.paused);
    try { console.debug('[xhsd] renderSidebar items:', items.length, 'paused:', paused); } catch {}
    const downloading = items.filter(i => i.status === 'downloading').length;
    const queued = items.filter(i => i.status === 'queued').length;
    const completed = items.filter(i => i.status === 'completed').length;
    const errored = items.filter(i => i.status === 'error').length;
    overlayStatus.innerHTML = ''; // Clear previous
    const statusItems = document.createDocumentFragment();


    const createStatusPart = (label, count) => {
      if (count === 0 && label !== '排队') return null;
      const el = document.createElement('span');
      el.style.cssText = 'display: inline-flex; align-items: center; gap: 4px;';
      const countEl = document.createElement('strong');
      countEl.textContent = count;
      countEl.style.cssText = 'font-size: 12px;';
      el.appendChild(document.createTextNode(label + ':'));
      el.appendChild(countEl);
      return el;
    };

    const parts = [
      createStatusPart('下载', downloading),
      createStatusPart('排队', queued),
      createStatusPart('完成', completed),
      createStatusPart('错误', errored)
    ].filter(Boolean);

    parts.forEach((part, index) => {
      statusItems.appendChild(part);
      if (index < parts.length - 1) {
        const sep = document.createElement('span');
        sep.textContent = '·';
        sep.style.color = '#d1d5db';
        statusItems.appendChild(sep);
      }
    });

    overlayStatus.appendChild(statusItems);
    overlayStatus.style.display = 'flex';
    overlayStatus.style.gap = '8px';
    overlayStatus.style.alignItems = 'center';

    if (overlayStartBtn && overlayPauseBtn && overlayClearBtn) {
      const actionable = (queued > 0) || (downloading > 0);
      if (!actionable) {
        overlayStartBtn.disabled = false;
        overlayPauseBtn.disabled = true;
      } else {
        overlayStartBtn.disabled = !paused;
        overlayPauseBtn.disabled = paused;
      }
      overlayClearBtn.disabled = (completed + errored) === 0;
    }

    overlayList.innerHTML = '';
    for (const it of items) {
      const row = document.createElement('div');
      row.style.cssText = 'display:flex;align-items:center;gap:6px;padding:6px 4px;border-bottom:1px solid #f3f4f6;';
      const name = document.createElement('div');
      name.textContent = (it.filename || it.url || '').toString().split(/[\\\//]/).pop();
      name.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
      const statusContainer = document.createElement('div');
      statusContainer.style.cssText = 'width: 80px; text-align: right;';

      const progressWrapper = document.createElement('div');
      progressWrapper.style.cssText = 'height: 4px; background-color: #e5e7eb; border-radius: 2px; margin-top: 4px; overflow: hidden;';
      const progressBar = document.createElement('div');
      progressBar.style.cssText = 'height: 100%; width: 0%; transition: width 0.2s linear;';
      progressWrapper.appendChild(progressBar);

      const statusText = document.createElement('span');
      statusText.style.cssText = 'font-size:10px; color:#374151;';

      const progress = Math.round(it.progress || 0);
      progressBar.style.width = `${progress}%`;

      switch (it.status) {
        case 'queued':
          statusText.textContent = '排队';
          progressBar.style.backgroundColor = '#a1a1a1';
          progressBar.style.width = '0%';
          break;
        case 'downloading':
          statusText.textContent = `${progress}%`;
          progressBar.style.backgroundColor = '#2c974b';
          break;
        case 'completed':
          statusText.textContent = '完成';
          progressBar.style.backgroundColor = '#2c974b';
          break;
        case 'error':
          statusText.textContent = `错误: ${it.error || '未知'}`;
          statusText.title = it.error || '未知错误';
          progressBar.style.backgroundColor = '#d73a49';
          break;
      }

      const deleteBtn = document.createElement('button');
      deleteBtn.textContent = '×';
      deleteBtn.dataset.url = it.url;
      deleteBtn.style.cssText = 'border: none; background: transparent; color: #9ca3af; font-size: 16px; cursor: pointer; padding: 0 4px; line-height: 1;';

      row.appendChild(name);
      statusContainer.appendChild(statusText);
      statusContainer.appendChild(progressWrapper);
      row.appendChild(statusContainer);
      row.appendChild(deleteBtn);
      overlayList.appendChild(row);
    }
  }

  // Listen for background queue updates to render in sidebar
  try {
    chrome.runtime.onMessage.addListener((msg) => {
      if (!msg) return;
      if (msg.type === 'QUEUE_UPDATED') {
        try { console.debug('[xhsd] QUEUE_UPDATED received. queue:', (msg.queue||[]).length, 'paused:', !!msg.paused); } catch {}
        // Only update UI if sidebar is already present
        if (overlayRoot) {
          renderSidebar({
            queue: Array.isArray(msg.queue) ? msg.queue : [],
            paused: !!msg.paused,
            baseSubfolder: msg.baseSubfolder || ''
          });
        }
      } else if (msg.type === 'SHOW_SIDEBAR') {
        ensureOverlay();
        if (overlayRoot) overlayRoot.style.display = 'flex';
        chrome.runtime.sendMessage({ type: 'GET_QUEUE_STATE' }, (resp) => {
          try { console.debug('[xhsd] GET_QUEUE_STATE (SHOW_SIDEBAR) resp queue:', resp && resp.queue ? resp.queue.length : 0); } catch {}
          if (resp) renderSidebar(resp);
        });
      } else if (msg.type === 'HIDE_SIDEBAR') {
        if (overlayRoot) overlayRoot.style.display = 'none';
      }
    });
    // Request initial state (keeps sidebar data fresh even if hidden)
    chrome.runtime.sendMessage({ type: 'GET_QUEUE_STATE' }, (resp) => {
      try { console.debug('[xhsd] GET_QUEUE_STATE (init) resp queue:', resp && resp.queue ? resp.queue.length : 0); } catch {}
      // Do not auto-render unless overlay already exists
      if (resp && overlayRoot) renderSidebar(resp);
    });


  } catch {}
  const USE_OVERLAY = false; // progress will be shown in extension popup
  const overlayByDownloadId = new Map();
  let lastTriggerTs = 0;
  let lastTriggerType = '';
  let lastHintTs = 0;
  const recentDownloads = new Map(); // url -> ts
  function markAndCheckRecent(url, windowMs = 5000) {
    try {
      const now = Date.now();
      // cleanup
      for (const [u, ts] of Array.from(recentDownloads.entries())) {
        if (now - ts > windowMs) recentDownloads.delete(u);
      }
      const key = String(url || '');
      const ts = recentDownloads.get(key);
      if (ts && now - ts < windowMs) return true; // seen recently
      recentDownloads.set(key, now);
      return false;
    } catch { return false; }
  }

  async function handleAltClickLikeEvent(e) {
    const target = e.target;
    const media = findMediaElementFromEvent(e) || findMediaElement(target);
    const now = Date.now();

    // Non-modifier hint
    if (!e.altKey && !e.ctrlKey) {
      try {
        if (media && now - lastHintTs > 6000) {
          toast('提示：Ctrl+点击加入待下载；Alt+点击立即下载');
          lastHintTs = now;
        }
      } catch {}
      return;
    }

    if (!media) {
      if (e.altKey || e.ctrlKey) {
        try { toast('未识别到图片/视频，请直接点击媒体本身'); } catch {}
      }
      return;
    }

    // Filter out avatar/icon/small images; always allow videos
    const isVideo = media.tagName === 'VIDEO';
    const isImgLike = media.tagName === 'IMG' || media.tagName === 'PICTURE';
    function isAvatarLike(el) {
      try {
        const names = ((el.className || '') + ' ' + (el.id || '')).toLowerCase();
        const badHints = ['avatar', 'profile', 'icon', 'logo', 'badge', 'head', 'header', 'face'];
        if (badHints.some(k => names.includes(k))) return true;
        // climb a bit
        let p = el.parentElement, steps = 0;
        while (p && steps < 4) {
          const n = ((p.className || '') + ' ' + (p.id || '')).toLowerCase();
          if (badHints.some(k => n.includes(k))) return true;
          p = p.parentElement; steps++;
        }
      } catch {}
      return false;
    }
    function isSmall(el) {
      try {
        const r = el.getBoundingClientRect();
        const w = Math.max(r.width || 0, el.naturalWidth || 0);
        const h = Math.max(r.height || 0, el.naturalHeight || 0);
        return w < 64 || h < 64; // be less strict; still avoid tiny avatars/icons
      } catch { return true; }
    }
    if (!isVideo && isImgLike) {
      if (isAvatarLike(media) || isSmall(media)) {
        return; // ignore non-content images
      }
    }

    const { url, filename } = await extractMediaUrlAndFilename(media);
    if (!url) {
      toast('未找到可下载的媒体链接');
      return;
    }

    // De-dupe between pointerdown and click for the same gesture
    if (lastTriggerType && (e.type === 'click' || e.type === 'pointerdown')) {
      if (now - lastTriggerTs < 600) return;
    }
    lastTriggerTs = now;
    lastTriggerType = e.type;

    // Suppress page handlers when we take over
    if (e.cancelable) {
      e.preventDefault();
      if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
      e.stopPropagation();
    }

    const noteTitle = getNoteTitle();
    const mediaType = (media.tagName === 'IMG' || media.tagName === 'PICTURE') ? 'image' : (media.tagName === 'VIDEO' ? 'video' : '');

    // Strong de-dupe by URL within a short window
    if (markAndCheckRecent(url, 6000)) {
      try { toast('已在下载处理中'); } catch {}
      return;
    }

    // Helper with retry for MV3
    const sendMsg = (payload, retries = 2) => new Promise((resolve) => {
      chrome.runtime.sendMessage(payload, (response) => {
        const err = chrome.runtime.lastError;
        if (err) {
          const m = err.message || '';
          if ((m.includes('The message port closed before a response was received') || m.includes('Receiving end does not exist')) && retries > 0) {
            setTimeout(() => sendMsg(payload, retries - 1).then(resolve), 120);
            return;
          }
          if (!m.includes('The message port closed before a response was received') && !m.includes('Receiving end does not exist')) {
            toast('无法发送请求：' + m);
          }
        }
        resolve(response);
      });
    });

    if (e.ctrlKey && !e.altKey) {
      // Queue only
      const resp = await sendMsg({
        type: 'QUEUE_MEDIA',
        url,
        filename,
        mediaType,
        noteTitle
      });
      try {
        if (resp && resp.ok && resp.added) {
          toast('已加入待下载列表');
        } else if (resp && resp.ok === true && resp.added === false) {
          toast('已在待下载列表中');
        } else {
          toast('加入队列失败，请重试');
        }
      } catch {}
      return;
    }

    if (e.altKey) {
      // Immediate download (bypass queue)
      // Only react to primary button to avoid context/menu clicks
      if (typeof e.button === 'number' && e.button !== 0) return;

      toast('开始立即下载…');
      const resp = await sendMsg({
        type: 'IMMEDIATE_DOWNLOAD',
        url,
        filename,
        mediaType,
        noteTitle
      });

      // If background is not reachable or indicates failure, fallback to direct <a download>
      const failed = !resp || (resp.ok === false);
      if (failed) {
        try {
          const a = document.createElement('a');
          a.href = url;
          // Try to preserve filename hint
          const base = (filename || '').toString().trim() || 'download';
          a.download = base;
          a.rel = 'noopener noreferrer';
          a.target = '_blank';
          // Some sites block direct clicks; attach in DOM briefly
          document.body.appendChild(a);
          a.click();
          setTimeout(() => { try { a.remove(); } catch {} }, 0);
          toast('已触发浏览器下载');
        } catch (err) {
          toast('下载触发失败：' + (err && err.message ? err.message : '未知错误'));
        }
      } else {
        try { toast('已开始下载'); } catch {}
      }
      return;
    }
  }

  document.addEventListener('click', handleAltClickLikeEvent, true);
  // Some sites suppress click; also listen to pointerdown as fallback
  document.addEventListener('pointerdown', handleAltClickLikeEvent, true);

  function normalizeXhsImageUrl(u) {
    try {
      if (!u) return u;
      const url = new URL(u);
      // 对 xhscdn 不再去掉 '!' 后缀，保持与页面一致，避免 403；扩展名在后台依据 Content-Type 决定
      return url.toString();
    } catch {
      return u;
    }
  }

  function pickMetaContent(selectors) {
    for (const sel of selectors) {
      const el = document.querySelector(sel);
      if (el && el.content) return el.content;
    }
    return '';
  }

  function cleanTitleText(t) {
    if (!t) return '';
    let s = String(t).trim();
    // Remove site suffixes like “- 小红书” or “| 小红书” and trailing noise
    s = s.replace(/[\-|｜]\s*小红书.*/i, '').replace(/小红书\s*$/i, '').trim();
    return s;
  }

  function getNoteTitle() {
    // 1) Prefer og:title
    const og = pickMetaContent(['meta[property="og:title"]', 'meta[name="og:title"]', 'meta[name="twitter:title"]']);
    if (og) return sanitizeFilename(cleanTitleText(og));
    // 2) Common XHS title selectors (best-effort)
    const h1 = document.querySelector('h1, h1[class*="title"], h1.note-title');
    if (h1 && h1.textContent) return sanitizeFilename(cleanTitleText(h1.textContent));
    const selCandidates = [
      '[data-xhs-title]',
      '.note-title',
      '.title',
      'header [class*="title"]',
    ];
    for (const sel of selCandidates) {
      const el = document.querySelector(sel);
      if (el && el.textContent) return sanitizeFilename(cleanTitleText(el.textContent));
    }
    // 3) Try ld+json headline/name
    try {
      const ld = document.querySelector('script[type="application/ld+json"]');
      if (ld && ld.textContent) {
        const data = JSON.parse(ld.textContent);
        const t = data.headline || data.name || data.articleSection || '';
        if (t) return sanitizeFilename(cleanTitleText(t));
      }
    } catch {}

    // 4) Regex scan inline scripts for title fields
    try {
      const scripts = Array.from(document.scripts || []);
      const re = /"title"\s*:\s*"([^"]{2,120})"/g; // naive but effective
      for (const s of scripts) {
        const txt = s.textContent || '';
        if (!txt) continue;
        let m;
        while ((m = re.exec(txt)) !== null) {
          const cand = m[1];
          if (cand && !/小红书/.test(cand)) {
            return sanitizeFilename(cleanTitleText(cand));
          }
        }
      }
    } catch {}

    // 5) Fallback to document.title
    const dt = cleanTitleText(document.title || '');
    if (dt) return sanitizeFilename(dt);
    return '';
  }

  function extractFromInitialState() {
    // Try to find a JSON with video URLs
    const scripts = Array.from(document.scripts || []);
    const reMp4 = /(https?:\/\/[^"'\s]+\.(mp4|mov|webm)(\?[^"'\s]*)?)/ig;
    for (const s of scripts) {
      const txt = s.textContent || '';
      if (!txt) continue;
      if (/__INITIAL_STATE__|window\.__APOLLO_STATE__|window\.__SSR_DATA__/i.test(txt)) {
        let m;
        while ((m = reMp4.exec(txt)) !== null) {
          if (m[1]) return m[1];
        }
      }
    }
    return '';
  }


  chrome.runtime.onMessage.addListener((message) => {
    if (!message || !message.type) return;

    if (message.type === 'DOWNLOAD_STARTED') {
      if (USE_OVERLAY) overlayByDownloadId.set(message.downloadId, lastOverlay());
    }

    if (message.type === 'DOWNLOAD_PROGRESS') {
      if (!USE_OVERLAY) return;
      const overlay = overlayByDownloadId.get(message.downloadId);
      if (!overlay) return;

      const { bytesReceived, totalBytes, state, error } = message;

      if (typeof bytesReceived === 'number' && typeof totalBytes === 'number' && totalBytes > 0) {
        const pct = Math.max(0, Math.min(100, Math.round((bytesReceived / totalBytes) * 100)));
        overlay.bar.style.width = pct + '%';
        overlay.label.textContent = pct + '%';
      }

      if (state === 'complete') {
        overlay.bar.style.width = '100%';
        overlay.label.textContent = '完成';
        setTimeout(() => {
          removeOverlay(overlay);
          overlayByDownloadId.delete(message.downloadId);
        }, 800);
      } else if (state === 'interrupted') {
        overlay.container.classList.add('xhsd-error');
        overlay.label.textContent = '失败' + (error ? `: ${error}` : '');
        setTimeout(() => {
          removeOverlay(overlay);
          overlayByDownloadId.delete(message.downloadId);
        }, 1500);
      }
    }

    if (message.type === 'DOWNLOAD_ERROR') {
      if (USE_OVERLAY) {
        const overlay = lastOverlay();
        if (overlay) {
          overlay.container.classList.add('xhsd-error');
          overlay.label.textContent = '错误';
          setTimeout(() => removeOverlay(overlay), 1200);
        }
      }
      toast('下载失败：' + (message.error || '未知错误'));
    }
  });

  function findMediaElementFromEvent(e) {
    const path = (typeof e.composedPath === 'function' && e.composedPath()) || [];
    let firstImg = null;
    for (const n of path) {
      if (!n || !n.tagName) continue;
      if (n.tagName === 'VIDEO') return n;
      if (!firstImg && (n.tagName === 'IMG' || n.tagName === 'PICTURE')) firstImg = n;
    }
    return firstImg;
  }

  function findMediaElement(node) {
    if (!node) return null;
    if (node.tagName === 'IMG' || node.tagName === 'VIDEO' || node.tagName === 'PICTURE') return node;
    // Traverse up to find parent IMG/VIDEO
    let el = node;
    for (let i = 0; i < 10 && el; i++) {
      el = el.parentElement;
      if (!el) break;
      if (el.tagName === 'IMG' || el.tagName === 'VIDEO' || el.tagName === 'PICTURE') return el;
      // Some sites use div-background images; try to get via CSS background-image (best-effort)
      const bg = getComputedStyle(el).backgroundImage;
      const m = /url\(["']?([^"')]+)["']?\)/.exec(bg || '');
      if (m) {
        const fake = new Image();
        fake.src = m[1];
        return fake; // treat as IMG-like element
      }
      // role=img containers
      if (el.getAttribute && el.getAttribute('role') === 'img') {
        const ariaImg = el.querySelector('img');
        if (ariaImg) return ariaImg;
      }
      // anchor with direct image href
      if (el.tagName === 'A' && el.href && /\.(jpg|jpeg|png|gif|webp|avif)(\b|$)/i.test(el.href)) {
        const fake = new Image();
        fake.src = el.href;
        return fake;
      }
    }
    return null;
  }

  async function extractMediaUrlAndFilename(media) {
    if (!media) return {};
    let url = media.src;
    let filename = '';
    
    if (media.tagName === 'VIDEO') {
      // Prefer http(s) source over blob:
      const srcs = [];
      if (media.currentSrc) srcs.push(media.currentSrc);
      if (media.src) srcs.push(media.src);
      const dataSrc = media.getAttribute('data-src');
      if (dataSrc) srcs.push(dataSrc);
      media.querySelectorAll('source').forEach((s) => {
        if (s.src) srcs.push(s.src);
        const ds = s.getAttribute('data-src');
        if (ds) srcs.push(ds);
      });
      url = (srcs.find((u) => /^https?:\/\//i.test(u)) || '') || '';
      if (!url) {
        // handle protocol-relative
        const rel = srcs.find((u) => /^\/\//.test(u));
        if (rel) url = 'https:' + rel;
      }
      if (!url) {
        // Fallback: try to extract MP4 URL from page scripts/meta
        console.log('[XHS Downloader] Attempting to extract video URL from page scripts...');
        url = extractVideoUrlFromPage(media);
        if (url) {
          console.log('[XHS Downloader] Found video URL from page scripts:', url);
        } else {
          console.log('[XHS Downloader] No video URL found in page scripts');
        }
      }
      if (!url) {
        // As a last resort, inject a page-context scraper to read window globals
        const fromPage = await scrapeUrlsFromPageContext().catch(() => null);
        if (fromPage && fromPage.mp4 && fromPage.mp4.length) url = fromPage.mp4[0];
        if (!url && fromPage && fromPage.m3u8 && fromPage.m3u8.length) {
          const hls = fromPage.m3u8[0];
          try {
            await navigator.clipboard.writeText(hls);
            toast('仅检测到 HLS（m3u8），已复制地址到剪贴板');
          } catch {
            toast('仅检测到 HLS（m3u8），请使用下载工具粘贴该地址');
          }
        }
        // Try performance entries & network hook cache
        if (!url) {
          const perf = scanPerformanceForVideo();
          if (perf && perf.length) url = perf[0];
        }
        if (!url) {
          try { toast('正在抓取视频地址…请稍等'); } catch {}
          // try to trigger network by playing the video shortly
          try { media.play && media.play().catch(()=>{}); } catch {}
          const tapped = await tapNetworkForVideoOnce(5000).catch(() => null);
          if (tapped && tapped.length) url = tapped[0];
        }
      }
      filename = suggestNameFromVideo(media) || 'xhs-video.mp4';
    } else if (media.tagName === 'IMG' || media.tagName === 'PICTURE') {
      // Handle image elements
      url = pickBestSrc(media) || pickLazyImageSrc(media) || media.src || '';
      if (!url && media.tagName === 'PICTURE') {
        // For picture elements, try to find the best source
        const sources = media.querySelectorAll('source');
        for (const source of sources) {
          if (source.srcset) {
            const candidates = source.srcset.split(',').map(s => s.trim());
            if (candidates.length > 0) {
              url = candidates[candidates.length - 1].split(/\s+/)[0]; // Take the last (highest res) candidate
              break;
            }
          }
        }
        // Fallback to img within picture
        if (!url) {
          const img = media.querySelector('img');
          if (img) url = pickBestSrc(img) || img.src || '';
        }
      }
      url = normalizeXhsImageUrl(url);
      filename = suggestNameFromAltOrSrc(media, url);
    }
    
    return { url, filename };
  }

  function pickBestSrc(img) {
    if (img.currentSrc) return img.currentSrc;
    if (img.srcset) {
      // choose the highest resolution candidate
      const candidates = img.srcset.split(',').map((s) => s.trim());
      let best = '';
      let bestWidth = 0;
      for (const c of candidates) {
        const parts = c.split(/\s+/);
        const u = parts[0];
        const w = parts[1] && parts[1].endsWith('w') ? parseInt(parts[1]) : 0;
        if (w >= bestWidth) {
          bestWidth = w;
          best = u;
        }
      }
      if (best) return best;
    }
    return img.src || '';
  }

  function pickLazyImageSrc(img) {
    const attrs = ['data-src', 'data-original', 'data-origin', 'data-actualsrc', 'data-lazy-src'];
    for (const a of attrs) {
      const v = img.getAttribute(a);
      if (v) return v;
    }
    return '';
  }

  function suggestNameFromAltOrSrc(img, urlForExt = '') {
    const alt = (img.getAttribute('alt') || '').trim();
    const u = img.currentSrc || img.src || '';
    // Prefer big note title as the filename
    const title = getNoteTitle();
    const base = title || (alt ? sanitizeFilename(alt) : extractBaseName(u)) || '小红书图片';
    let ext = guessExtFromUrl(urlForExt || u);
    if (!ext) ext = '.jpg'; // 优先给出用户常见可视格式
    return base + ext;
  }

  function suggestNameFromVideo(video) {
    const aria = (video.getAttribute('aria-label') || '').trim();
    const title = getNoteTitle();
    if (title) return title + '.mp4';
    if (aria) return sanitizeFilename(aria) + '.mp4';
    const src = video.currentSrc || video.src || '';
    const base = extractBaseName(src) || 'xhs-video';
    const ext = guessExtFromUrl(src) || '.mp4';
    return base + ext;
  }

  function extractBaseName(u) {
    try {
      const url = new URL(u);
      const p = url.pathname.split('/').filter(Boolean).pop() || '';
      const name = p.split('?')[0].split('#')[0];
      return prettifyBaseName(name.replace(/\.[a-z0-9]{2,6}$/i, ''));
    } catch (e) {
      return '';
    }
  }

  function guessExtFromUrl(u) {
    const lower = (u || '').toLowerCase();
    if (/(\.jpg|\.jpeg|\.png|\.gif|\.webp|\.avif)(\b|$)/.test(lower)) return lower.match(/\.(jpg|jpeg|png|gif|webp|avif)/i)[0];
    // xhscdn: path may be like .../spectrum/xxxxx!nd_dft_wlteh_webp_3 -> prefer jpg fallback
    if (/xhscdn\.com/.test(lower)) return '.jpg';
    if (/(\.mp4|\.mov|\.webm)(\b|$)/.test(lower)) return lower.match(/\.(mp4|mov|webm)/i)[0];
    if (lower.includes('image')) return '.jpg';
    if (lower.includes('video')) return '.mp4';
    return '';
  }

  

  function sanitizeFilename(name) {
    return (name || '')
      .replace(/[\\/:*?"<>|]/g, '_')
      .replace(/\s+/g, ' ')
      .trim()
      .slice(0, 100);
  }

  function prettifyBaseName(name) {
    try {
      // Remove CDN suffix like !nd_dft_wlteh_webp_3
      const noBang = (name || '').split('!')[0];
      // Decode percent-encoded
      const decoded = decodeURIComponent(noBang);
      return sanitizeFilename(decoded);
    } catch (e) {
      return sanitizeFilename(name || '');
    }
  }

  function getShortTitlePrefix() {
    const t = (document.title || '').trim();
    if (!t) return '';
    // Keep it short to avoid long names
    const short = t.replace(/[\r\n]+/g, ' ').trim().slice(0, 30);
    return sanitizeFilename(short);
  }

  function joinName(a, b) {
    if (a && b) return `${a} - ${b}`;
    return a || b || '';
  }

  function extractVideoUrlFromPage(videoEl) {
    // Collect candidates from meta, ld+json, and inline scripts (XHS often embeds JSON)
    const mp4s = [];
    const m3u8s = [];

    // meta tags
    const metaVideo = pickMetaContent([
      'meta[property="og:video:secure_url"]',
      'meta[property="og:video:url"]',
      'meta[property="og:video"]',
      'meta[name="og:video"]'
    ]);
    if (metaVideo) (/\.m3u8/i.test(metaVideo) ? m3u8s : mp4s).push(metaVideo);

    // ld+json
    document.querySelectorAll('script[type="application/ld+json"]').forEach((s) => {
      try {
        const data = JSON.parse(s.textContent || '{}');
        const v = data.contentUrl || data.video?.contentUrl;
        if (typeof v === 'string') (/\.m3u8/i.test(v) ? m3u8s : mp4s).push(v);
      } catch {}
    });

    // Inline scripts: look for structured keys first (h264.url etc.)
    const scripts = Array.from(document.scripts || []);
    const reH264 = /\bh264\b[^{}]*?\burl\b\s*:\s*"(https?:\/\/[^"']+)"/ig;
    const reUrlList = /url_list"?\s*:\s*\[(.*?)\]/ig;
    const reUrlInList = /"url"\s*:\s*"(https?:\/\/[^"']+)"/ig;
    const reMp4 = /(https?:\/\/[^"'\s]+xhscdn[^"'\s]+\.(mp4|mov|webm)(\?[^"'\s]*)?)/ig;
    const reGenericMp4 = /(https?:\/\/[^"'\s]+\.(mp4|mov|webm)(\?[^"'\s]*)?)/ig;
    const reM3u8 = /(https?:\/\/[^"'\s]+\.m3u8(\?[^"'\s]*)?)/ig;
    const reEscapedMp4 = /https?:\\\/\\\/[^"\s]+\.(mp4|mov|webm)(\?[^"\s]*)?/ig; // escaped in JSON
    const reEscapedM3u8 = /https?:\\\/\\\/[^"\s]+\.m3u8(\?[^"\s]*)?/ig;
    const reKeyedUrl = /\b(playUrl|play_url|play|url|videoUrl|video_url|src)\b\s*[:=]\s*"(https?:\\\/\\\/[^"]+)"/ig;
    
    // Enhanced patterns for newer XHS formats
    const reVideoData = /"video"[^{}]*?"url"[^"]*?"(https?:[^"]+)"/ig;
    const reStreamUrl = /"stream"[^{}]*?"url"[^"]*?"(https?:[^"]+)"/ig;
    const reMediaUrl = /"media"[^{}]*?"url"[^"]*?"(https?:[^"]+)"/ig;
    const reAdaptationSet = /"adaptationSet"[^{}]*?"url"[^"]*?"(https?:[^"]+)"/ig;
    const reVideoInfo = /"videoInfo"[^{}]*?"url"[^"]*?"(https?:[^"]+)"/ig;
    
    const unescapeJsonUrl = (u) => {
      try { return u.replace(/\\\//g, '/'); } catch { return u; }
    };
    
    for (const s of scripts) {
      const txt = s.textContent || '';
      if (!txt) continue;
      let m;
      
      // Original patterns
      while ((m = reH264.exec(txt)) !== null) mp4s.push(m[1]);
      while ((m = reUrlList.exec(txt)) !== null) {
        const seg = m[1] || '';
        let m2;
        while ((m2 = reUrlInList.exec(seg)) !== null) mp4s.push(m2[1]);
      }
      while ((m = reMp4.exec(txt)) !== null) mp4s.push(m[1]);
      while ((m = reGenericMp4.exec(txt)) !== null) mp4s.push(m[1]);
      while ((m = reM3u8.exec(txt)) !== null) m3u8s.push(m[1]);
      while ((m = reEscapedMp4.exec(txt)) !== null) mp4s.push(unescapeJsonUrl(m[0]));
      while ((m = reEscapedM3u8.exec(txt)) !== null) m3u8s.push(unescapeJsonUrl(m[0]));
      while ((m = reKeyedUrl.exec(txt)) !== null) {
        const u = unescapeJsonUrl(m[2]);
        (/\.m3u8/i.test(u) ? m3u8s : mp4s).push(u);
      }
      
      // Enhanced patterns for newer formats
      while ((m = reVideoData.exec(txt)) !== null) {
        const u = unescapeJsonUrl(m[1]);
        (/\.m3u8/i.test(u) ? m3u8s : mp4s).push(u);
      }
      while ((m = reStreamUrl.exec(txt)) !== null) {
        const u = unescapeJsonUrl(m[1]);
        (/\.m3u8/i.test(u) ? m3u8s : mp4s).push(u);
      }
      while ((m = reMediaUrl.exec(txt)) !== null) {
        const u = unescapeJsonUrl(m[1]);
        (/\.m3u8/i.test(u) ? m3u8s : mp4s).push(u);
      }
      while ((m = reAdaptationSet.exec(txt)) !== null) {
        const u = unescapeJsonUrl(m[1]);
        (/\.m3u8/i.test(u) ? m3u8s : mp4s).push(u);
      }
      while ((m = reVideoInfo.exec(txt)) !== null) {
        const u = unescapeJsonUrl(m[1]);
        (/\.m3u8/i.test(u) ? m3u8s : mp4s).push(u);
      }
    }

    // Inspect the video element and its sources for data attributes
    if (videoEl) {
      const candAttrs = ['data-url', 'data-src', 'data-video', 'data-h264', 'data-mp4', 'data-stream-url', 'data-video-url'];
      for (const a of candAttrs) {
        const v = videoEl.getAttribute(a);
        if (v) {
          if (/^\/\//.test(v)) mp4s.push('https:' + v);
          else if (/^https?:/i.test(v)) (/\.m3u8/i.test(v) ? m3u8s : mp4s).push(v);
        }
      }
      
      // Check video element's parent containers for data attributes
      let parent = videoEl.parentElement;
      let depth = 0;
      while (parent && depth < 3) {
        for (const a of candAttrs) {
          const v = parent.getAttribute(a);
          if (v) {
            if (/^\/\//.test(v)) mp4s.push('https:' + v);
            else if (/^https?:/i.test(v)) (/\.m3u8/i.test(v) ? m3u8s : mp4s).push(v);
          }
        }
        parent = parent.parentElement;
        depth++;
      }
    }

    // From initial state helper (mp4s only)
    const fromInit = extractFromInitialState();
    if (fromInit) (/\.m3u8/i.test(fromInit) ? m3u8s : mp4s).push(fromInit);

    const normalize = (u) => (u && /^\/\//.test(u) ? 'https:' + u : u);
    const prefer = (arr) => {
      const norm = (arr || []).map(normalize).filter(u => u && u.length > 10);
      return norm.find((u) => /xhscdn\.com/i.test(u)) || norm[0];
    };
    const mp4 = prefer(mp4s || []);
    if (mp4) return mp4;

    const hls = prefer(m3u8s || []);
    if (hls) {
      toast('检测到 HLS 流（m3u8），当前版本仅支持单文件视频(MP4)。');
      return '';
    }
    return '';
  }

  // Overlay UI helpers
  let __lastOverlay = null;
  function lastOverlay() {
    return __lastOverlay;
  }

  function createOverlay(rect) {
    const container = document.createElement('div');
    container.className = 'xhsd-overlay';
    container.style.position = 'fixed';
    container.style.left = Math.max(8, rect.left + window.scrollX) + 'px';
    container.style.top = Math.max(8, rect.top + window.scrollY) + 'px';

    const barWrap = document.createElement('div');
    barWrap.className = 'xhsd-bar-wrap';

    const bar = document.createElement('div');
    bar.className = 'xhsd-bar';

    const label = document.createElement('div');
    label.className = 'xhsd-label';
    label.textContent = '0%';

    barWrap.appendChild(bar);
    container.appendChild(barWrap);
    container.appendChild(label);

    __lastOverlay = { container, barWrap, bar, label };
    return __lastOverlay;
  }

  function removeOverlay(overlay) {
    if (overlay?.container && overlay.container.parentNode) {
      overlay.container.parentNode.removeChild(overlay.container);
    }
    if (__lastOverlay === overlay) __lastOverlay = null;
  }

  function toast(text) {
    const el = document.createElement('div');
    el.className = 'xhsd-toast';
    el.textContent = text;
    document.body.appendChild(el);
    setTimeout(() => el.classList.add('show'), 10);
    setTimeout(() => {
      el.classList.remove('show');
      setTimeout(() => el.remove(), 300);
    }, 2000);
  }
})();
